C++ 快速学习
Table of Contents
函数与参数
传值参数
int abc(int a, int b, int c) { return a+b+b*c+(a+b-c)/(a+b)+4; }
在运行时, 实际值会在函数执行前被复制给形参, 复制过程由形参对应的数据类型的 拷贝构造函数
来完成, 当函数结束时, 形参所属数据类型的 析构函数
负责释放该形参. 所以当一个函数返回时, 形参的值不会被复制到对应的实参中, 所以, 传值情况下, 函数调用不会修改实际参数的值.
模板函数
将上面的函数改成用模板来实现, 可以在多种数据类型之间通用, 将参数的数据类型作为一个变量, 它的值由编译器来确定. 如:
template<typename T> T abc(T a, T b, T c) { return a+b+b*c+(a+b-c)/(a+b)+4; } // ... // 调用 int x=2, y=4; abc(2, x, y);
引用参数
使用传值参数, 会在一定程度上降低程序的效率. 假设数据类型是用户自定义的 Matrix 类, 其拷贝构造函数将复制所有元素, 析构函数将释放所有元素. 如果我们用具有 1000 个元素的 Matrix 作为实际参数来调用 abc(), 则复制给 3 个参数需要 3000 次操作, 析构时又要进行 3000 次操作.
如果使用引用参数, 在函数被调用时, 不会复制实参的值, 函数调用会修改实参的值.
可以将上面的模板函数改成:
template<typename T> T abc(T& a, T& b, T& c) { return a+b+b*c+(a+b-c)/(a+b)+4; } // ... // 调用 int x = 1, y = 2; cout << abc(x, x, y) << endl;
常量引用参数
使用常量引用参数, 可以使得函数不能修改引用参数的值. 这在软件工程方面具有重要的意义, 用户可以立即了解到该函数并不会修改实际参数.
template<typename T> T abc(const T& a, const T& b, const T& c) { return a+b+b*c+(a+b-c)/(a+b)+4; }
如果想让程序更加通用, 可以使用三种类型, 如: template<typename T1, typename T2, typename T3>.
返回值
在以上的函数中, 返回值就是一个具体值, 这意味着返回的对象会被复制到调用环境中. 只要在释放临时变量以及形参的空间之前, 将该值返回, 就不会丢失这个值.
如果需要返回引用, 可以这么写:
template<typename T> T& func(int i, T& z) { // ... return z; }
当函数返回时, 传值形参 i 及其他局部变量将被释放, 而 z 是对实际参数的引用, 不会受到影响.
如果这么写, 则返回值会丢失:
int& func(int i, int& z) { // ... int* ip = &i; return ip; // 丢失 }
如果要返回常量引用, 可以这么写:
template<typename T> const T& func(int i, T& z) { // ... }
递归函数
递归函数就是自己调用自己, 有直接递归和间接递归两种.
直接递归: 函数 F 中直接包含了函数 F.
间接递归: 函数 F 中包含函数 G, 函数 G 又调用函数 H, ..., 最后一个函数又调用了函数 F.
C++ 允许我们编写递归函数, 递归函数必须包含终止条件. 如计算阶乘:
int Factorial(int n) { if (n <= 1) return 1; // 终止条件 return n*Factorial(n-1); }
动态存储分配
new
new 操作符会返回一个指向所分配空间的指针. 如果要给一个整数动态分配存储空间, 并在刚分配的空间中存储 10 这个整数, 可以这么写:
int *y = new int; *y = 10; // 或者 int *y = new int(10);
一维数组
一个大小为 n 的一维浮点数组可以这样创建:
float *x = new float[n]; // 返回第 2 个元素 x[1];
异常处理
try catch 机制可以捕获异常. 我们将可能会出现异常的代码放在 try{...} 中, 对异常的处理放在 catch(){...} 中. catch() 可以有多个, 针对不同的异常情况, 进行不同的处理, 如果想直接捕获所有异常, 则这么写: catch(...){}.
在 new 操作时, 如果内存不够分配了, 将会出现异常, 抛出的异常类型为 xalloc, 所以上面的动态内存分配, 可以这么写:
try { float *x = new float[10]; } catch (xalloc) { cerr << "out of memory" << endl; exit(1); }
如果没有引发异常, 执行完 try 块后, 直接跳过 catch 块.
delete
与 new 相对应的释放操作. 释放方法如下:
delete y; delete [] x;
二维数组
二维数组在声明时必须确定第二维的大小, 第一维可以动态确定. 如: a[]1 是合法的, 而 a[][] 则不是.
char (*c)[5]; try { c = new char [n][5]; } catch (xalloc) { cerr << "out of memory" << endl; exit(1); }
template<typename T> bool Make2DArray (T ** &x, int rows, int cols) { // 创建一个二维数组 try{ x = new T * [rows]; // 创建行指针 for (int i=0; i<rows; i++) { x[i] = new int [cols]; return true; } } catch (xalloc) { return false; } } template<typename T> vod Delete2DArray (T ** &x, int rows) { for (int i=0; i<rows; i++) { delete [] x[i]; delete [] x; x = 0; } }
类
货币类 Currency 的声明
使用三个变量来描述一个货币: 符号(+-), 美元(dollars), 美分(cents).
#ifndef CURRENCY_H #define CURRENCY_H namespace currency{ enum sign { plus, minus }; class Currency{ public: Currency(sign s=plus, unsigned long d=0, unsigned int c=0); // 构造函数 ~Currency(){} // 析构函数 bool Set(sign s, unsigned long d, unsigned int c); bool Set(float a); sign Sign() const { return sgn; } unsigned long Dollars() const { return dollars; } unsigned int Cents() const { return cents; } Currency Add(const Currency& x) const; Currency& Increment(const Currency& x); void Output() const; private: sign sgn; unsigned long dollars; unsigned int cents; }; } #endif
这边使用了名字空间, 是因为 plus 和 minus 在标准库中也用到了, 直接使用会出现冲突.
构造函数
构造函数与类名同名, 它指定了如何创建一个给定类型的对象, 不可以有返回值. 在创建一个 Currency 类对象时, 构造函数被自动调用.
创建 Currency 类对象的方式:
Currency f, g(plus, 3, 45), h(minus, 10); Currency *m = new Currency(plus, 8, 12);
析构函数
析构函数比类名多一个符号 ~. 当 Currency 对象超出作用域时, 自动调用析构函数, 用来删除对象. 在这个例子中, 析构函数是空的, 但是如果有些类创建了动态数组, 则需要在析构函数中释放这些空间. 析构函数也没有返回值.
拷贝构造函数
Add 函数和 Increment 函数都会返回 Currency 类对象, 但 Add 函数返回的是值, Increment 函数返回的是引用.
拷贝构造函数用来执行返回值的复制及传值参数的复制. 在例子中, 我们没有给出拷贝构造函数, 所以 C++ 会使用默认的拷贝构造函数, 默认的拷贝构造函数进行数据成员的复制, 如果需要做其他工作, 则需要自己实现.
货币类 Currency 的实现
#include"currency.h" #include<iostream> #include<cstdlib> using namespace currency; //构造函数 Currency::Currency(sign s, unsigned long d, unsigned int c) { if (c > 99) { std::cerr << "Cents should be < 100" << std::endl; exit(1); } sgn = s; dollars = d; cents = c; } // 设置 private 数据成员 bool Currency::Set(sign s, unsigned long d, unsigned int c) { if (c > 99) return false; sgn = s; dollars = d; cents = c; return true; } bool Currency::Set(float a) { if (a < 0) { sgn = minus; a = -a; } else { sgn = plus; } dollars = a; // 抽取整数部分 // 形如 a.bc 这种格式的数字, 在计算机中可能没有一个精确的表示. 例如, 计算机所描述的数字 5.29, 可能 // 比 5.29 稍微小一点. 所以对 (a-dollars)*100 得到的数进行取整, 得到的会是 28, 而不是 29. 由于 // 我们只要取两位数, 所以将其加上 0.005. 如果要取三位数, 则加上 0.0005. cents = (a + 0.005 - dollars) * 100; return true; } // 累加两个 Currency // 计算机进行小数运算时, 会丢失精度, 所以将两个 Currency 进行累加时, 先将其转化成整数, 再进行计算 Currency Currency::Add(const Currency& x) const { long a1, a2, a3; Currency ans; a1 = dollars * 100 + cents; if (sgn == minus) a1 = -a1; a2 = x.dollars * 100 + x.cents; if (x.sgn == minus) a2 = -a2; a3 = a1 + a2; if (a3 < 0) { ans.sgn = minus; a3 = -a3; } else { ans.sgn = plus; } ans.dollars = a3 / 100; ans.cents = a3 - ans.dollars * 100; return ans; } Currency& Currency::Increment(const Currency& x) { // this 是指向当前对象的指针, 所以 *this 表示当前对象 *this = Add(x); return *this; } void Currency::Output() const { if (sgn == minus) { std::cout << '-'; } std::cout << '$' << dollars << '.'; if (cents < 10) { std::cout << "0"; } std::cout << cents; }
由于 Currency 类的数据成员都是私有的, 所以用户不能通过如下语句来改变这些数据成员的值:
h.cents = 20; h.dollars = 100; h.sgn = plus;
而是只能通过构造函数和 Set 函数来进行设置, 这两个函数会判断数据成员是否合法, 而 Add 等函数则不再验证.
货币类 Currency 的使用
#include <iostream> #include "currency.h" using namespace currency; int main(int argc, char const *argv[]) { /* code */ Currency g, h(plus, 3, 50), i, j; g.Set(minus, 2, 25); i.Set(-6.45); j = h.Add(g); j.Output(); std::cout << std::endl; j = i.Add(g).Add(h); j.Output(); std::cout << std::endl; j = i.Increment(g).Add(h); j.Output(); std::cout << std::endl; i.Output(); std::cout << std::endl; return 0; }
Makefile
main: main.o currency.o g++ main.o currency.o -o main main.o: main.cpp g++ -c main.cpp -o main.o currency.o: currency.cpp g++ -c currency.cpp -o currency.o clean: rm *.o rm main
运算符重载
在 Currency 类中, Add 函数和 Increment 函数相当于平时用到的 + 和 += 操作符, 直接使用这两个操作符, 会更加自然. 另外, Output 函数相当于 << 操作符.
操作符重载允许我们对 C++ 操作符进行扩展, 使其能应用到新的数据类型或类.
为简单起见, 从现在开始, 我们的 Currency 类只有一个私有数据成员: long amount, $1.32 直接用数字 132 来表示.
Currency operator+(const Currency& x) const { Currency y; y.amount = amount + x.amount; return y; } Currency& operator+=(const Currency& x) { amount += x.amount; return *this; } void Output(ostream& out) const { long a = amount; if (a < 0) { out << '-'; a = -a; } long d = a / 100; out << '$' << d << '.'; int c = a - d * 100; if (c < 10) out << "0"; out << c; } ostream& operator<<(ostream& out, const Currency& x) { x.Output(output); return out; } // 使用 cout << i << endl; // i 是 Currency 对象
引发异常
像构造函数和 Set 函数, 有可能会在执行预定的任务时失败. 可以定义一个异常类, 在引发异常时, 捕获异常并处理.
class BadInitializers { // 定义异常类 public: BadInitializers() {} };
Currency::Currency(sign s, unsigned long d, unsigned int c) { if (c > 99) throw BadInitializers(); // ... } void Currency::Set(sign s, unsigned long d, unsigned int c) { if (c > 99) throw BadInitializers(); // ... }
友元函数
一个类的 private 成员仅对于本类的成员函数是可见的. 但是有时候, 必须把对这些 private 成员的访问权限授予其他的类和函数, 于是就有了友元函数.
像前面的 << 运算符重载中, Output 函数是 Currency 类的成员函数, operator<< 则不是, 所以编写时会比较麻烦, 要在 operator<< 函数内部调用 Output 函数.
有了友元函数, 我们可以直接使用 operator<<, 而不再需要 Output 函数.
class Currency{ public: friend ostream& operator<<(ostream&, const Currency&); }; ostream& operator<<(ostream& out, const Currency& x) { long a = x.amount; if (a < 0) { out << '-'; a = -a; } long d = a / 100; out << '$' << d << '.'; int c = a - d * 100; if (c < 10) out << "0"; out << c; return out; }
为什么不能直接将 operator<< 设置为成员函数?
因为流操作符的左边必须是成员函数所属的类, 但是实际上, 流操作符都是 cin, cout, cerr 等, 这不是我们所能修改的, 所以最好的办法是将其定义为友元.
protected
#ifndef, #define, #endif 语句
建议为每个头文件都加上这些语句, 这样可以确保这些头文件仅被包含和编译一次.
测试
黑盒测试
假设程序不可见, 仅设计输入和期望的输出.
白盒测试
程序可见, 设计测试数据, 使得程序的每一条语句都至少被执行一次(如 if 分支, 都要跑一遍).
Footnotes:
DEFINITION NOT FOUND.
Generated by Emacs 25.x(Org mode 8.x)
Copyright © 2014 - Pinvon - Powered by EGO